iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 13

Day-013. 一些讓你看來很強的全端 TRPC 伴讀 -Prefetch & Cache

  • 分享至 

  • xImage
  •  

今天要來講 trpc cache 機制,在 trpc 中有兩種方式 :

  1. SSR: true ,透過 getInitialProps 幫頁面做 preRenender。
  2. server side helper。( 本人推薦 )

SSR: true 介紹

因為 getInitialProps 是早期在 getServerSideProps 還沒出現前的替代方式,trpc 為了整合這部分功能所以才加上,如果需要啟用只需要在 trpc client 中設定就好。

getInitialProps 前情提要

getInitialProps 是一種獲取頁面資料的一種方式,也別於在 client side 透過 useEffect fetch data,getInitialProps 讓你可以在 server side 中提前幫你 fetch data ,不用等到 client side 才發送 get datarequest 而達到 preFetch 效果,而 getInitialProps 這個 function 執行時機點可以在 client side server side

  • server side: 當 user 第一次進到頁面時,會在 server side 先執行第一次,透過序列化以及緩序列化將 data 存到 window.__NEXT_DATA__.props 中,並寫入 htmlinline script<script>window.__NEXT_DATA__={props:{xxx}}</script> 這樣 client sidecomponent 就可以透過 inline script 吃到這個 page 中會用到的 data,而不是去發 request api

  • client side: 當 user 是透過連結跳轉過去或是點選上一頁的時候,而不是透過貼上連結的方式時, getInitialProps 會在 client side 中去觸發,但因為 user 已經訪問過第一次,所以這時在 client 端中執行的 getInitialProps 會去讀取 window.__NEXT_DATA__.props 內容然後給 component 中使用。

至於 getInitialProps 怎麼更新,則是透過設置 requestcache-control 決定 revalidate 的時間點,那 trpc 怎麼做到下面 demo 會解釋。

getInitialProps 缺點

getInitialProps 目前的生態系中已經不是主流的寫法,取而代之的是透過 getServerSidePropsgetStaticProps 等方法,差別在於後者只會在 server 中拿取頁面會用到的資料以及 revalidae cache

getInitialProps 中還有一個致命缺點就是因為他會在 clientserver 都去執行,所以 getInitialProps 中會用到的 function 也會一並打包到 client 中,這會大大增加 js boundle size 的大小,所以這部分筆者不建議去用他。

SSR: true 設定

透過 ssr: true 設置,如此一來 trpc 預設將透過 getInitialProps preFetch 每個頁面調用的 useQuery

export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    if (typeof window !== 'undefined') {
      return {
        links: [
          httpBatchLink({
            url: '/api/trpc',
          }),
        ],
      };
    }
    const url = process.env.VERCEL_URL
      ? `https://${process.env.VERCEL_URL}/api/trpc`
      : 'http://localhost:3000/api/trpc';
    return {
      links: {
        http: httpBatchLink({
          url,
        }),
      },
    };
  },
  ssr: true,

如果你需要設置 revalidate trpc 有一個 responseMeta 可以設置 cache-control 時機點,但這部分必須使用 ssr: true 的功能。

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    if (typeof window !== 'undefined') {
      return {
        links: [
          httpBatchLink({
            url: '/api/trpc',
          }),
        ],
      };
    }
    const url = process.env.VERCEL_URL
      ? `https://${process.env.VERCEL_URL}/api/trpc`
      : 'http://localhost:3000/api/trpc';
    return {
      links: {
        http: httpBatchLink({
          url,
        }),
      },
    };
  },
  ssr: true,
  responseMeta(opts) {
    const { clientErrors } = opts;
    if (clientErrors.length) {
      // propagate http first error from API calls
      return {
        status: clientErrors[0].data?.httpStatus ?? 500,
      };
    }
    // 每秒進行一次 revalidate , cache 最多存一天
    const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
    return {
      headers: {
        'cache-control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
      },
    };
  },
});

server side helper

這部分是本人比較推薦的用法,有個 helper 就不需要依靠 getInitialProps 幫我們做 preFetch 而是能在 getServerSidePropsgetStaticProps 中自由使用。

首先先新增 posts/[id].tsx page ,然後在 posts page 加一個 click 轉址到 post details

// ~src/posts/[id].tsx
const PostDetail = ({ id }: PostDetailProps) => {
  const router = useRouter()
  const { data: post, status, ...rest } = api.posts.getPost.useQuery({ post_id: id })
  if (status !== 'success') {
    return <>Loading...</>
  }
  return (
    <>
      <h1>Post details Page</h1>
      <p>title : {post.title}</p>
      <p>id : {post.id}</p>
      <p>content : {post.content}</p>
      <button onClick={() => router.back()}>back to home</button>
    </>
  )
}

添加 IoIosArchive icon 做導頁。

// ~src/page/index.tsx
import { IoIosArchive } from "react-icons/io";

export default function Home() {
 // ..
  return (
// ..
          {posts.map((post, index) => (
            <li
              key={post.id}
              className="flex items-center justify-between cursor-pointer"
            >
              //..
              <div className="flex items-center gap-[1rem]">
                <AiFillDelete
                  color="red"
                  className="cursor-pointer"
                  size={20}
                  onClick={async () => {
                    await deletePost({ id: post.id })
                  }}
                />
                <IoIosArchive
                  onClick={() => router.push(`/posts/${post.id}`)}
                />
      //..
  );
}

然後呼叫 createServerSideHelpers 這樣你就可以在 server Side 中使用 trpc 的內容了。

export async function getStaticProps(
  context: GetStaticPropsContext<{ id: string }>,
) {
  const helpers = createServerSideHelpers({
    router: appRouter,
    ctx: {
      prisma
    },
  });
  // const id = context.params?.id as string;
  const id = context.params?.id as string

  // preFetch post data to PostDetail
  await helpers.posts.getPost.prefetch({ post_id: id })

  return {
    props: {
      trpcState: helpers.dehydrate(),
      id,
    },
    revalidate: 1,
  };
}

這邊 trpc 先做 prefetch 將資料送到 query cache 中。

 await helpers.posts.getPost.prefetch({ post_id: id })

並將 pararms.id 一並傳到 client。

 return {
    props: {
      trpcState: helpers.dehydrate(),
      id,
    },
     //..

因為 trpc 已經先 preFetch 過,所以在 PostDetail 中的 api.posts.getPost.useQuery 不會再重新發送一次 request 而是直接從 query cahce 中拿資料,所以 if (status !== 'success') 這裡的 function 永遠不會去觸發,也就不會再有 loading 的畫面。

// ~src/posts/[id].tsx
const PostDetail = ({ id }: PostDetailProps) => {
  const router = useRouter()
  const { data: post, status, ...rest } = api.posts.getPost.useQuery({ post_id: id })
  if (status !== 'success') {
    return <>Loading...</>
  }
    //..

那因為有加 revalidate: 1 ,所以只要 user 只要重新回到頁面時每秒都會幫你重新 update data

export async function getStaticProps(
  context: GetStaticPropsContext<{ id: string }>,
) {
 //..
  return {
    props: {
      trpcState: helpers.dehydrate(),
      id,
    },
    revalidate: 1,
  };
}

但假如如果頁面中資料並不太需要太頻繁的 revalidate data ,你可以把 revalidate 的時間調高,或是把 user 觸發 revalidate 行為關閉如下:

如果一來就不會因為 WindowReFocus 就打一次 api ,但這部分還是取決於你的需求是什麼去做調整,並沒有一定的答案。

const data = trpc.example.useQuery(
  // if your query takes no input, make sure that you don't
  // accidentally pass the query options as the first argument
  undefined,
  { refetchOnMount: false, refetchOnWindowFocus: false },
);

或是你可以在 trpc client 中設定。

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';
export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    return {
      transformer: superjson,
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
        }),
      ],
      // Change options globally
      queryClientConfig: {
        defaultOptions: {
          queries: {
            refetchOnMount: false,
            refetchOnWindowFocus: false,
          },
        },
      },
    },
  },
});

最後我們透過 npm build 來看看結果吧~

這樣 datainitial html 都先被 server side build 完成了,如此一來你就完成 preLoadpreFetch 的功能摟~

相關連結

https://github.com/Danny101201/next_demo/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day-012. 一些讓你看來很強的全端 TRPC 伴讀 - Links
下一篇
Day-014. 一些讓你看來很強的全端 TRPC 伴讀 -InfiniteQuery (上)
系列文
一些讓你看來很強的全端- trcp 伴讀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言